import rough from "roughjs";
import { Config, Drawable } from "roughjs/bin/core";
import { RoughGenerator } from "roughjs/bin/generator";
import { VNode } from "./vnode";

/**
 * A map between a type and the drawing function.
 */
const drawMap: {
  [type: string]: (g: RoughGenerator, node: VNode) => Drawable | Drawable[];
} = {
  line: (g, node) => {
    const { x1, y1, x2, y2 } = node.props;
    return g.line(
      toNumber(x1),
      toNumber(y1),
      toNumber(x2),
      toNumber(y2),
      node.props
    );
  },
  rect: (g, node) => {
    const { x, y, width, height } = node.props;
    return g.rectangle(
      toNumber(x),
      toNumber(y),
      toNumber(width),
      toNumber(height),
      node.props
    );
  },
  ellipse: (g, node) => {
    const { cx, cy, rx, ry, width, height } = node.props;
    return g.ellipse(
      toNumber(cx),
      toNumber(cy),
      typeof width !== "undefined" ? toNumber(width) : toNumber(rx) * 2,
      typeof height !== "undefined" ? toNumber(height) : toNumber(ry) * 2,
      node.props
    );
  },
  circle: (g, node) => {
    const { x, y, r } = node.props;
    return g.circle(toNumber(x), toNumber(y), toNumber(r) * 2, node.props);
  },
  linearPath: (g, node) => {
    const points = Array.isArray(node.props.points)
      ? node.props.points
      : `${node.props.points}`
          .split(" ")
          .map((p) => p.split(",").map(toNumber));
    return g.linearPath(points, node.props);
  },
  polygon: (g, node) => {
    const vertices = Array.isArray(node.props.vertices)
      ? node.props.vertices
      : `${node.props.vertices}`
          .split(" ")
          .map((p) => p.split(",").map(toNumber));
    return g.polygon(vertices, node.props);
  },
  arc: (g, node) => {
    const { x, y, width, height, start, stop, closed } = node.props;
    return g.arc(
      Number(x),
      Number(y),
      Number(width),
      Number(height),
      Number(start),
      Number(stop),
      Boolean(closed),
      node.props
    );
  },
  curve: (g, node) => {
    const points = Array.isArray(node.props.points)
      ? node.props.points
      : `${node.props.points}`
          .split(" ")
          .map((p) => p.split(",").map(toNumber));
    return g.curve(points, node.props);
  },
  path: (g, node) => {
    return g.path(node.props.d, node.props);
  },
  g: (g, node) => {
    const subDrawables = renderToDrawables(node.children);
    const translate = (node.props.transform as string).match(
      /translate\(\s*(\s*-?\d+)\s*,\s*(-?\d+)\s*\)/
    );
    if (translate) {
      const delta = translate.slice(1);
      subDrawables.forEach((d) =>
        d.sets.forEach((s) =>
          s.ops.forEach((o) => {
            o.data = o.data.map((da, i) => da + parseInt(delta[i % 2]));
          })
        )
      );
    }
    return subDrawables;
  },
};

/**
 * Convert numbers or throw error.
 *
 * @param raw - the raw type.
 * @returns number
 */
function toNumber(raw: any) {
  const n = parseInt(raw, 10);
  if (isNaN(n)) {
    throw new Error("number is expected but got NaN");
  }
  return n;
}

/**
 * Convert the virtual nodes into drawables.
 *
 * @param nodes - the virtual nodes.
 * @returns rough drawables.
 * @public
 */
export function renderToDrawables(nodes: VNode[], config?: Config) {
  const g = rough.generator(config);
  return nodes.reduce((drawables, node) => {
    const draw = drawMap[node.type];
    if (draw) {
      const subDrawables = draw(g, node);
      if (Array.isArray(subDrawables)) {
        drawables.push(...subDrawables);
      } else {
        drawables.push(subDrawables);
      }
    } else {
      drawables.push(...renderToDrawables(node.children));
    }
    return drawables;
  }, [] as Drawable[]);
}
